package tileStyle;

import java.io.*;
import java.util.*;
import java.util.concurrent.Semaphore;

import tileStyle.Prism.*;
import tileStyle.Prism.events.*;
import Prism.core.*;
import Prism.extensions.port.distribution.*;
import Prism.extensions.port.*;

/**
 * A Node represents a node on a sTile network. It can deploy tiles.
 * 
 * @author 					<a href=mailto:brun@cs.umass.edu>Yuriy Brun</a>
 * @version 				2014.06
 */
public class Node extends TileStyleComponent {
	
///////////////////////////////////////////////
// Member Variables
///////////////////////////////////////////////
	
	public static final long 				serialVersionUID 		= 1L;

	/**
	 * This node's address
	 */
	protected NodeAddress 					address;
	
	/**
	 * The type of tiles this node can deploy (used for computing and for input seeds)
	 */
	protected TileType 						type;
	
	/**
	 * The map of tiles deployed on this node
	 */
	protected Map<Integer, Tile> 			tiles;
	
	/**
	 * The index of a newly created tile
	 */
	protected int							tileIndex 				= 0;
	
	/**
	 * Semaphore for index number for tiles.
	 */
	protected Semaphore 					mLock 					= new Semaphore (1, true);
	
	/**
	 * The queue that maintains the list of tiles that need to be executed
	 */
	protected Executor						executor 				= new Executor(this);
	
	/**
	 * The map of where each type of a tile is deployed
	 */
	protected Map<TileType, List<Port>> 	typeLookup;
	
	/**
	 * The map of ports where each tile can be reached
	 */
	protected Map<Integer, Port> 			addressLookup;
	
	/**
	 * Set of ports (communication channels) with other nodes
	 */
	protected Set<Port> 					seedPorts;
	
	/**
	 * Handle of the file in which Mahjong writes when it finds the solution
	 */
	protected BufferedWriter 				outFile;
	
	/**
	 * Tile generation rate tracker
	 */
	protected TileGenRateTracker			tgrt;
	
	/**
	 * Tile generation rate tracker switch
	 */
	protected boolean						tgrt_on 				= false;
	
	
///////////////////////////////////////////////
// Member Variables: Prism-MW
///////////////////////////////////////////////
	
	protected Architecture 					architecture;
	protected SocketDistribution 			sd;
	protected FIFOScheduler 				scheduler;
	protected RRobinDispatcher 				dispatcher;
	protected ExtensiblePort 				inPort;
	
///////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////	
	
	/**
	 * Default constructor
	 * 
	 * @param name			This node's name
	 * @param type			The type of tile this node will maintain
	 * @param hostname		Host name of the server
	 * @param uniqueID		Unique ID of this node
	 * @param port			Port number of the server
	 * @param outFile		Handle of the file in which Mahjong writes when it finds the solution
	 */
	public Node(	String 			name, 
					TileType 		type, 
					String 			hostname, 
					int 			uniqueID, 
					int 			port, 
					BufferedWriter 	outFile) {
		super(name);
		this.type 		= type;
		this.outFile 	= outFile;
		
		tiles 			= Collections.synchronizedMap(new HashMap<Integer, Tile>());
		
		initializeArchitecture(port);
		
		address 		= new NodeAddress(hostname, uniqueID, port);
		
		// tile generation tracker -- comment out the line below to quiet it down
		if(tgrt_on) {
			tgrt 		= new TileGenRateTracker(this.toString(), 60000);	
		}
		
	}
	
	/**
	 * Alternative constructor that creates a new Node, randomly choosing among 
	 * the allowed tile types to deploy.
	 * 
	 * @param name
	 * @param types			The types of tiles this node might deploy
	 * @param hostname
	 * @param uniqueID
	 * @param port
	 * @param outFile
	 */
	public Node(	String 			name, 
					List<TileType> 	types, 
					String 			hostname, 
					int 			uniqueID, 
					int 			port, 
					BufferedWriter 	outFile) {
		this(	name, 
				types.get((int) (Math.random() * types.size())), 
				hostname, 
				uniqueID, 
				port, 
				outFile);
	}
	
///////////////////////////////////////////////
// Member Methods
///////////////////////////////////////////////	
	
	/**
	 * Initializes the Prism-MW architecture
	 * 
	 * @param portNum		Port number to be used for inPort
	 */
	private void initializeArchitecture(int portNum) {
		architecture 			= new Architecture("Node");
		scheduler 				= new FIFOScheduler();
		Scaffold scaff 			= new Scaffold();
		dispatcher 				= new RRobinDispatcher(scheduler, 10);
		
		scaff.dispatcher 		= dispatcher;
		scaff.scheduler 		= scheduler;
		scaffold 				= scaff;

		architecture.scaffold 	= scaffold;
		
		inPort 					= new ExtensiblePort("inPort" + portNum, PrismConstants.REPLY);
		sd 						= new SocketDistribution(inPort, portNum);
		
		inPort.addDistributionModule(sd);
		inPort.scaffold 		= scaffold;
		this.addCompPort(inPort);
		architecture.add(inPort);
		architecture.add(this);
		
		dispatcher.start();
		architecture.start();
	}
	
	public void createLookup(Map<TileType, List<NodeAddress>> addresses) throws IOException {
		typeLookup 				= Collections.synchronizedMap(new HashMap<TileType, List<Port>>());
		addressLookup 			= Collections.synchronizedMap(new HashMap<Integer, Port>());
		seedPorts 				= new HashSet<Port>();
		
		for (Iterator<TileType> types = addresses.keySet().iterator(); types.hasNext();) {
			TileType currentType 	= types.next();
			List<Port> ports 		= new ArrayList<Port>();
			for (Iterator<NodeAddress> currentAddresses = addresses.get(currentType).iterator(); currentAddresses.hasNext();) {
				NodeAddress currentAddress 	= currentAddresses.next();
				Port currentPort 			= null;
				
				if (currentAddress.isOnHost(address.getHostname())) {
					currentPort = new Port("out node local port", PrismConstants.REQUEST);
				} else {
					ExtensiblePort currentExtensiblePort 	= new ExtensiblePort("out node port", PrismConstants.REQUEST);
					SocketDistribution currentSD 			= new SocketDistribution(currentExtensiblePort);   
					currentExtensiblePort.addDistributionModule(currentSD);
					currentPort 							= currentExtensiblePort;
				}
				
				currentPort.scaffold = scaffold;
				this.addCompPort(currentPort);
				architecture.add(currentPort);
				ports.add(currentPort);

				if (currentPort instanceof ExtensiblePort) {
					if (TileStyleStarter.DEBUG) {
						System.out.println("connecting to:" + currentAddress.getHostname() + ":" + currentAddress.getPort());
					}
					((ExtensiblePort) currentPort).connect(currentAddress.getHostname(), currentAddress.getPort());
				} else {
					architecture.weld(currentPort, currentAddress.getNode().getNewPort());
				}
				currentPort.start();

				if (currentAddress.isForSeed()) {
					seedPorts.add(currentPort);
				}
				addressLookup.put(currentAddress.getUniqueID(), currentPort);
			}
			typeLookup.put(currentType, ports);
		}
	}
	
	/**
	 * Creates a new Prism-MW port
	 * 
	 * @return				Newly created Prism-MW port
	 */
	public Port getNewPort() {
		Port port = new Port("incoming port for local communication", PrismConstants.REPLY);
		addCompPort(port);
		architecture.add(port);
		port.start();
		return port;
	}

	/**
	 * Gets this node's address
	 * 
	 * @return				This node's address
	 */
	public NodeAddress getAddress() {
		return address;
	}
	
	/**
	 * Looks up which node maintains given type of tiles
	 * 
	 * @param type			Type of tiles to look for 
	 * @return				Port at which the node can be reached
	 */
	private Port lookupNode(TileType type) {
		List<Port> valids = (List<Port>) typeLookup.get(type);
		Port valid;
		
		do {
			valid = valids.get((int) (Math.random() * valids.size()));
		} while (seedPorts.contains(valid));
		
		return valid;
	}
	
	/**
	 * Updates this node's neighbors
	 * 
	 * @param 	parentNeighborsAddress	Parent neighbor's address
	 * @param 	assemblyID				Tile assembly ID
	 * @param 	direction				Direction
	 * @param 	fromInNodeID			The index (ID) of the tile
	 */
	public void updateNeighbor(	TileAddress parentNeighborsAddress, 
								int assemblyID, 
								int direction, 
								int fromInNodeID) {
		Port destination 			= addressLookup.get(parentNeighborsAddress.getUniqueID());
		ChildUpdateRequest event 	= new ChildUpdateRequest(	assemblyID, 
																direction, 
																fromInNodeID, 
																parentNeighborsAddress.getPortID(), 
																address);
		
		// sends a message to the parentNeighbor asking for its proper child
		send(event, destination);
	}
	
	/**
	 * Deploys a new tile in this node
	 * 
	 * @param 	type				Type of tile to deploy
	 * @param 	assemblyID			Tile assembly ID
	 * @param 	isSeed				Whether it is a seed tile
	 * @return						Index of the newly created tile in the tiles array
	 * @throws 	TileStyleException	Thrown when trying to deploy a tile of an incompatible type 
	 */
	public int deployTile(TileType type, int assemblyID, boolean isSeed) throws TileStyleException {
		
		// checks if the type of the new tile matches this node
		if (!(type.equals(type))) {
			throw new TileStyleException("Trying to deploy a tile of an incompatible type");
		}
		
		// creates a new tile
		Tile tile = new Tile(type, this, assemblyID, isSeed);
	
		// gets the index of the new tile
		int index = getNextTileIndex();
		tiles.put(new Integer (index), tile);
		tile.setInNodeID((int) index);
		
		// prints the new tile information
		/*
		System.out.println(		System.currentTimeMillis() + ", " + 
								assemblyID + ", " +
								type.getName() + ", " + 
								this.toString() + ", " + 
								tiles.size() + ", " +
								executor.size());
		*/
		
		// increases the tile generation counter
		if(tgrt_on) {
			tgrt.increase();
		}
		

		// checks if Mahjong has found the solution
		if(type.getName().endsWith("checkcheck")) {
			// creates the message to print
			String message = "Found solution at " + System.currentTimeMillis();
			
			// prints the message to screen
			System.out.println(message);
			
			// prints the message to file
			try {
				if (outFile != null) {
					outFile.write(message);
					outFile.newLine();
					outFile.close();
				}
			} catch (IOException e) {
				throw new TileStyleException("Failed to write the solution found message to file: " + e.getMessage());
			}
			
			// quits
			System.exit(0);
		}
		
		// adds the newly created tile to the queue of tile execution
		executor.add(tile);
		
		// returns the index of the newly created tile
		return index;
	}

	/**
	 * Replicates a tile
	 * 
	 * @param 	original			Tile to replicate
	 * @throws 	TileStyleException	The tile is not ready to replicate
	 */
	private void replicate(Tile original) throws TileStyleException {
		// prints debug message
		if (TileStyleStarter.DEBUG) {
			System.out.println("replicating");
		}
		
		// throws an exception when the tile is not ready to replicate
		if (!(original.isReadyToReplicate())) {
			throw new TileStyleException("Tile " + original + " is trying to replicate without being ready");
		}
		
		// finds new hosts
		Port copyHostPort = lookupNode(original.type);
		
		// gets original neighbors
		TileAddress[] neighbors = new TileAddress[4];
		for (int i = 0; i < 4; i++) {
			neighbors[i] = original.getNeighbor(i);
		}
						
		// creates a replicate request
		CreateReplicateRequest crr = new CreateReplicateRequest(	type, 
																	original.getAssemblyID() + 1, 
																	neighbors, 
																	original.getInNodeID(), 
																	address);
		
		// sends the replicate request to the new host
		send(crr, copyHostPort);			
	}
	
	/**
	 * Executes the recruiting procedure. Throws a TileTypeException if corner is not 
	 * ready to recruit. Otherwise, finds a new tile, at random from the lookup map that
	 * deploys tiles that can attach to the north west of corner.
	 * 
	 * @param 	corner				The tile to the south east of an empty location
	 * @throws 	TileStyleException	The corner tile is not ready to recruit
	 */
	public void recruit(Tile corner) throws TileStyleException {

		// checks if the corner tile is ready to recruit
		if (!(corner.isReadyToRecruit())) {
			throw new TileStyleException("Tile " + corner + " is trying to recruit without being ready");
		}

		TileAddress north 	= corner.getNeighbor(TileType.NORTH);
		TileAddress west 	= corner.getNeighbor(TileType.WEST);
		Port destination 	= addressLookup.get(north.getUniqueID());
		
		send(new GetNorthsSideEvent(north, west, corner.getAddress()), destination);
	}
	
	/**
	 * Handles incoming Prism-MW events
	 * 
	 * @param	event				Incoming Prism-MW event
	 */
	public synchronized void handle(Event event) {
		if (TileStyleStarter.DEBUG) {
			System.out.println("handling event of type " + event.getClass());
		}
		
		/////////////////////////// Handling ChildUpdateRequest
		if (event instanceof ChildUpdateRequest) {
			
			// retrieves the corresponding tile
			ChildUpdateRequest	e 		= (ChildUpdateRequest) event;
			Tile 				tile 	= tiles.get(e.getToInNodeID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): ChildUpdateRequest cannot find the corresponding tile");
				return;
			}
			
			int childNum = e.getAssemblyID() - tile.getAssemblyID() - 1;
			if (childNum == 0) {
				TileAddress childAddress 		= tile.getChild(childNum);
				ChildUpdateResponse response 	= new ChildUpdateResponse(	childAddress, 
																			e.getDirection(), 
																			e.getAssemblyID(), 
																			e.getFromInNodeID());
				
				if (childAddress != null) {
					send(response, addressLookup.get(e.getReturnAddress().getUniqueID()));
				} else {
					// stores the info necessary to be sent back until the child is created
					ChildUpdateResponsePacket packet = new ChildUpdateResponsePacket(	response, 
																						addressLookup.get(e.getReturnAddress().getUniqueID()), 
																						childNum);
					tile.addChildUpdateResponsePacket(packet);
				}
			} else {
				throw new TileStyleException("tried to update a BAD assembly ID");
			}
		}
		
		/////////////////////////// Handling ChildUpdateResponse
		else if (event instanceof ChildUpdateResponse) {
			
			// retrieves the corresponding tile
			ChildUpdateResponse	e 		= (ChildUpdateResponse) event;
			Tile 				tile 	= tiles.get(e.getToInNodeID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): ChildUpdateResponse cannot find the corresponding tile");
				return;
			}
			
			if (!((e.getAddress() == null) && (tile.getNeighbor(e.getDirection()) != null))) {
				if (e.getAssemblyID() == tile.getAssemblyID()) {
					if (e.getAddress() != null) {
						tile.setNeighbor(e.getAddress(), e.getDirection());
					} else {
						if (tile.getStatus() > Tile.UPDATING) {
							throw new TileStyleException("Got a null neighbor update AFTER declaring done updating");
						} else {
							tile.setStatus(Tile.UNREPLICATED);
						}
					}
				} else {
					// else the neighbor has already been set but a more recent event, and we don't want to erase it
					throw new TileStyleException("tried to send back a BAD assembly ID child");
				}
			}
			
			// adds the tile to the execution queue
			executor.add(tile);
		}
		
		/////////////////////////// Handling CreateReplicateRequest
		else if (event instanceof CreateReplicateRequest) {
			
			// deploys a new tile
			CreateReplicateRequest	e 				= (CreateReplicateRequest) event;
			int						newbornIndex	= deployTile(e.getType(), e.getAssemblyID(), true);
			Tile 					newborn 		= tiles.get(newbornIndex);
			
			// checks if the tile exists
			if(newborn == null) {
				System.out.println("Error at handle(): ChildReplicateRequest cannot find the corresponding tile");
				return;
			}
			
			// sets the parent neighbors
			TileAddress[] parentNeighbors = e.getNeighbors();
			for (int i = 0; i < 4; i++) {
				newborn.setParentNeighbor(parentNeighbors[i], i);
			}
			
			// sends a CreateReplicateReply
			send(new CreateReplicateReply(newborn.getAddress(), e.getAssemblyID(), e.getFromInNodeID()), addressLookup.get(e.getReturnAddress().getUniqueID()));
		}
		
		/////////////////////////// Handling CreateReplicateReply
		else if (event instanceof CreateReplicateReply) {
			
			// retrieves the corresponding tile
			CreateReplicateReply 	e 		= (CreateReplicateReply) event;
			Tile 					tile 	= tiles.get(e.getToInNodeID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): ChildReplicateReply cannot find the corresponding tile");
				return;
			}
			
			int childNum = e.getAssemblyID() - tile.getAssemblyID() - 1;
			if (childNum == 0) {
				tile.setChild(e.getAddress(), childNum);
			} else {
				throw new TileStyleException("tried to update a BAD assembly ID");
			}
			
			// adds the tile to the execution queue
			executor.add(tile);
		}
		
		/////////////////////////// Handling GetNorthsSideEvent
		else if (event instanceof GetNorthsSideEvent) {
			
			// retrieves the corresponding port and tile
			GetNorthsSideEvent	e 			= (GetNorthsSideEvent) event;
			Port 				destination	= addressLookup.get(e.getWestAddress().getUniqueID());
			Tile 				tile 		= tiles.get(e.getNorthAddress().getPortID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): GetNorthsSideEvent cannot find the corresponding tile");
				return;
			}
			
			// sends a GetWestsSideEvent
			send(new GetWestsSideEvent(e.getNorthAddress(), e.getWestAddress(), e.getHomeAddress(), tile.getType().getWest()), destination);
			
			// adds the tile to the execution queue
			executor.add(tile);
		}
		
		/////////////////////////// Handling GetWestsSideEvent
		else if (event instanceof GetWestsSideEvent) {
			
			// retrieves the corresponding port and tile
			GetWestsSideEvent	e 			= (GetWestsSideEvent) event;
			Port 				destination	= addressLookup.get(e.getHomeAddress().getUniqueID());
			Tile 				tile 		= tiles.get(e.getWestAddress().getPortID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): GetWestsSideEvent cannot find the corresponding tile");
				return;
			}
			
			// sends a GotSidesEvent
			send(new GotSidesEvent(e.getNorthAddress(), e.getWestAddress(), e.getHomeAddress(), e.getNorthsSide(), tile.getType().getNorth()), destination);
			
			// adds the tile to the execution queue
			executor.add(tile);
		}
		
		/////////////////////////// Handling GotSidesEvent
		else if (event instanceof GotSidesEvent) {
			
			// retrieves the corresponding tile
			GotSidesEvent	e 		= (GotSidesEvent) event;
			Tile 			corner 	= tiles.get(e.getHomeAddress().getPortID());
			
			// checks if the tile exists
			if(corner == null) {
				System.out.println("Error at handle(): GotSidesEvent cannot find the corresponding tile");
				return;
			}
			
			// finds the tile type
			TileType type = TileType.getNorthWestMatch(	typeLookup.keySet().iterator(), 
												  		e.getNorthsSide(), 
												  		e.getWestsSide());
			
			if (type != null) {
				Port attachmentNodePort = lookupNode(type);
				send(new AttachTileEvent(e.getNorthAddress(), e.getWestAddress(), corner.getAssemblyID(), type), attachmentNodePort);
			}

			// else there is no matching tile
			corner.setStatus(Tile.RECRUITED);
		}
		
		/////////////////////////// Handling AttachTileEvent
		else if (event instanceof AttachTileEvent) {
			
			// deploys a new tile
			AttachTileEvent	e 				= (AttachTileEvent) event;
			int 			newbornIndex	= deployTile(e.getType(), e.getAssemblyID(), false);
			Tile 			newborn 		= tiles.get(newbornIndex);
			
			// checks if the tile exists
			if(newborn == null) {
				System.out.println("Error at handle(): AttachTileEvent cannot find the corresponding tile");
				return;
			}

			// sets the neighbors for the new tile
			newborn.setNeighbor(e.getNorthAddress(), TileType.EAST);
			newborn.setNeighbor(e.getWestAddress(), TileType.SOUTH);
			TileAddress newbornAddress = new TileAddress(address.getHostname(), address.getUniqueID(), address.getPort(), (int) newbornIndex);

			// finds ports for south and east
			Port south 	= addressLookup.get(e.getWestAddress().getUniqueID());
			Port east 	= addressLookup.get(e.getNorthAddress().getUniqueID());

			// sends SetNeighborEvent events
			send(new SetNeighborEvent(newbornAddress, e.getWestAddress(), TileType.NORTH), south);
			send(new SetNeighborEvent(newbornAddress, e.getNorthAddress(), TileType.WEST), east);
		}
		
		/////////////////////////// Handling SetNeighborEvent
		else if (event instanceof SetNeighborEvent) {
			
			// retrieves the corresponding tile
			SetNeighborEvent e = (SetNeighborEvent) event;
			Tile tile = tiles.get(e.getAddress().getPortID());
			
			// checks if the tile exists
			if(tile == null) {
				System.out.println("Error at handle(): SetNeighborEvent cannot find the corresponding tile");
				return;
			}
			
			// sets the neighbor
			tile.setNeighbor(e.getNewbornAddress(), e.getDirection());
			
			// adds the tile to the execution queue
			executor.add(tile);
		}
		
		/////////////////////////// Handling KeepAlive
		else if (event instanceof KeepAlive) {
			// do nothing
		}
		
		/////////////////////////// Handling the rest of the events
		else throw new TileStyleException("Unknown event type" + event.getClass());
	}

	/**
	 * Starts the execution loop
	 */
	public synchronized void execute() {
		// starts the executor
		executor.start();
	}
	
	/**
	 * Executes a single tile
	 * 
	 * @param tile			Tile to execute
	 * @return				Tile that needs to be put back in the {@link Executor}. Could be null.
	 */
	public synchronized Tile executeTile(Tile tile) {
		Tile returnTile = null;
		
		switch(tile.getStatus()) {
			case Tile.UNREPLICATED:
				tile.updateNeighbors();
				tile.setStatus(Tile.UPDATING);
				returnTile = tile;
				break;
			case Tile.UPDATING:
				if (tile.isReadyToReplicate()) {
					tile.setStatus(Tile.READY_TO_REPLICATE);
					returnTile = tile;
				} 
				break;
			case Tile.READY_TO_REPLICATE:
				if(tile.isReadyToReplicate()) {
					replicate(tile);
					tile.setStatus(Tile.REPLICATED);
					returnTile = tile;
				}
				else {
					throw new TileStyleException("Tried to replicate when it was not ready.");
					//returnTile = tile;
				}
				break;
			case Tile.REPLICATED:
				if (tile.isReadyToRecruit()) {
					recruit(tile);
					tile.setStatus(Tile.RECRUITING);
					returnTile = tile;
				} 
				break;
			case Tile.RECRUITED:
				// do nothing
				break;
		}
		
		return returnTile;
	}
	
	
	
	/**
	 * Locks the semaphore
	 */
	protected void getLock() {
		try {
			mLock.acquire();	// get the semaphore
		} catch (InterruptedException ie) {
			// do nothing
		}
	}
	
	/**
	 * Releases the semaphore
	 */
	protected void releaseLock() {
		mLock.release();
	}
	
	/**
	 * Returns the next tile index
	 * 
	 * @return				Index for a new tile
	 */
	protected int getNextTileIndex() {
		int index = 0;
		
		getLock();
		index = tileIndex++;
		releaseLock();
		
		return index;
	}
}
